# -*- encoding: utf-8 -*-

# Copyright 2010 István Szentandrási
# <etude-music-player@googlegroups.com>

# This file is part of Etude.
#
# Etude is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as 
# published by the Free Software Foundation, either version 3 
# of the License, or (at your option) any later version.
#
# Etude is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Etude. If not, see <http://www.gnu.org/licenses/>.

from . import _

import etude.plugins as ep
import etude.client as ec
import gtk
import gobject
HAVE_GL = True
try:
    from gtk.gtkgl.apputils import *
    import OpenGL
    import OpenGL.extensions as oglext
    import OpenGL.GL.shaders as oglsh
    import numpy as np
except ImportError:
    HAVE_GL = False

import visualization_wrapper as visw
DATA_SIZE = 1024
VIZ_TIMEOUT = 1000.0/100.0
import time
import math
import colorsys
from pkg_resources import resource_string

TEX_QUADS = 101


class Visualization(ep.PluginPanel):

    _type = ep.E_PANEL_TYPE_OTHER
    _short_name = "Visualiation (custom)"
    _short_name = _("Visualiation (custom)")
    _short_descr = _("Custom visualization for Etude.")
    _version = "0.1"
    _unique = "Visualization-0.1" # short name + version is enough, just to identify loaded
    _title = _("Visualization")
    _config = {'fps': '30'}
    __author__= "Szentandrási István"

    """
    Custom visualization for etude
    """

    def __init__(self, control):
        ep.PluginPanel.__init__(self, control)
        if not HAVE_GL:
            print("Sry libraries missing")
            self.unload()
            return
        self.data = np.array([], 'f')

        self.visscene = VisScene(self)
        self.glarea = GLArea(self.visscene)
        self.add(self.glarea)
        
        self.xmms_vis = visw.VisWrapper(self)
        print("initializing visualization")
        self.xmms_vis.init_visualization({'type': 'pcm', 'stereo': "1"})
        
        print("timeout: %f" % (1000.0/float(self.get_config('fps'))))
        rendertime = 1000.0/float(self.get_config('fps'))
        self.enabled = True
        self.chunk_timer = gobject.timeout_add(int(VIZ_TIMEOUT), self.get_data)
        self.timer = gobject.timeout_add(int(rendertime), self.refresh)

        self.show_all()

    def get_data(self):
        data = self.xmms_vis.chunk_get(0, 0, DATA_SIZE)
        if len(data) == DATA_SIZE:
            self.visscene.process_data(np.array(data, 'f'))
        elif self.control.client.playback.status != ec.PB_PLAY:
            self.visscene.process_data(np.zeros(DATA_SIZE))
        return self.enabled

    def refresh(self):
        #rendertime = 1000.0/float(self.get_config('fps'))
        #self.visscene.refresh()
        if(len(self.visscene.data) == DATA_SIZE):
            try:
                self.glarea.window.invalidate_rect(self.glarea.allocation, False)
            except:
                return False
        return self.enabled
    
    def unload(self):
        ep.PluginPanel.unload(self)
        self.enabled = False

class VisScene(GLScene, GLSceneButton, GLSceneKey):

    """
    Currently every texture is the size of the of the screen - should change
    it to constant values, and let the user choose the resolution
    """

    def __init__(self, plugin):
        GLScene.__init__(self,
                gtk.gdkgl.MODE_RGB |
                gtk.gdkgl.MODE_DEPTH |
                gtk.gdkgl.MODE_DOUBLE |
                gtk.gdkgl.MODE_ALPHA)
        GLSceneButton.__init__(self)
        GLSceneKey.__init__(self)
        self.plugin = plugin
        self.shader_program = None
        self.fullscreen = False

        self.rotx = 0
        self.roty = 0
        self.rotz = 0.0
        self.trotx = 0.0
        self.troty = 0.0
        self.trotz = 0.0
        self.ttransx = 0.0
        self.ttransy = 0.0
        self.ttransz = 0.3
        self.ratio = 1
        self.width = 600
        self.height = 400
        self.is_solid = True

        self.data = np.array([])

        self.simple_beat_detection = True
        self.simple_beat_detected = False
        self.simple_stored = np.zeros((100, 4))

        self.beats_to_detect = 28
        self.beat_detection = False
        self.beat_detected = np.zeros(self.beats_to_detect, 'b')
         
        #self.beat_limits = np.array(
        #        [ 2, 4, 7, 10, 14, 18, 22, 27,
        #            32, 37, 43, 49, 56, 63, 70, 78,
        #            86, 95, 104, 113, 123, 133, 144, 155,
        #            166, 178, 190, 202, 215, 228, 242, 256], 'i')
        #logaritmic
        self.beat_limits = np.array( 
                [1, 2, 3, 4, 5, 6, 7, 8, 
                    10, 12, 14, 16, 19, 22, 27, 32,
                    38, 45, 54, 64, 76, 90, 107, 128,
                    152, 181, 215, 256], 'i')
        self.stored_fftl = np.zeros((100,self.beats_to_detect))
        self.stored_fftr = np.zeros((100,self.beats_to_detect))
        self.palette = []
        for i in xrange(self.beats_to_detect):
            self.palette.append(colorsys.hsv_to_rgb(0.7-i*1.0/self.beats_to_detect, 1.0, 1.0))

    def key_press(self, width, height, event):
        if event.keyval in 'fF':
            #for some reason segfaulted
            if self.fullscreen:
                #print('setting unfullscreen')
                event.window.area.unfulscreen()
            else:
                #print('setting fullscreen')
                event.window.area.fulscreen()
                #self.glarea.get_window().fullscreen()
            self.fullscreen = not self.fullscreen

    def key_release(self, width, height, event):
        pass

    def set_actor(self, widget, data):
        if data == "PCM-simple":
            self.beat_detection = False
            self.simple_beat_detection = True
            self.activate_shader(False)
            self.display = self.pcm_show
        else:
            self.beat_detection = True
            self.simple_beat_detection = False
            self.activate_shader(True)
            self.display = self.beat_show
    
    def button_press(self, width, height, event):
        """
        Mouse butten press event handler
        """
        if event.button == 3:
            #show a menu
            menu = gtk.Menu()
            for lab in ("PCM-simple", "Beat-detect"):
                item = gtk.MenuItem(label= lab)
                item.connect('activate', self.set_actor, lab)
                menu.append(item)

            menu.show_all()
            menu.popup(None, None, None, 3, event.time, event.window)
        elif event.button == 2:
            if self.fullscreen:
                self.plugin.window.unfullscreen()
            else:
                self.plugin.window.fullscreen()
            self.fullscreen = not self.fullscreen

    def button_release(self, width, height, event):
        pass


    def init(self):
        """
        initiate the viewport, create textures
        and display lists
        """
        glViewport(0, 0, self.width, self.height)
        glDepthFunc(GL_LESS)
        glDisable(GL_DEPTH_TEST)
        glClearColor(0.0, 0.0, 0.0, 0.0)
        glClearAccum(0.0, 0.0, 0.0, 0.0)
        glClear(GL_ACCUM_BUFFER_BIT)
        glClear(GL_COLOR_BUFFER_BIT)
        glEnable(GL_LINE_SMOOTH)
        glEnable(GL_POINT_SMOOTH)
        self.init_texture()
        self.init_shader()
        self.compile_lists()
        self.set_actor(None, "Beat-detect")

    def compile_lists(self):
        glNewList(TEX_QUADS, GL_COMPILE)
        
        glBegin(GL_QUADS)
        glTexCoord2i(0,1)
        glVertex3f(-2.0, 2.0, 0.0)
        glTexCoord2i(0,0)
        glVertex3f(-2.0, -2.0, 0.0)
        glTexCoord2i(1,0)
        glVertex3f(2.0, -2.0, 0.0)
        glTexCoord2i(1,1)
        glVertex3f(2.0, 2.0, 0.0)
        glEnd()
        
        glEndList()

    def init_shader(self):
        vsh = resource_string(__package__, 'shaders/rings.vert')
        fsh = resource_string(__package__, 'shaders/rings.frag')

        self.shader_program = oglsh.compileProgram(
                oglsh.compileShader(vsh, oglsh.GL_VERTEX_SHADER),
                oglsh.compileShader(fsh, oglsh.GL_FRAGMENT_SHADER)
                )
        self.historyarr = np.zeros((32, 28))
    
    def activate_shader(self, active=True):
        if active and (self.shader_program is not None):
            glUseProgram(self.shader_program)
            loc = oglsh.glGetUniformLocation(self.shader_program, "screensize")
            oglsh.glUniform2f(loc, float(self.width), float(self.height))
            loc = oglsh.glGetUniformLocation(self.shader_program, "beat_size_vec")
            oglsh.glUniform1fv(loc, 28, np.zeros(28))
            loc = oglsh.glGetUniformLocation(self.shader_program, "currentTime")
            oglsh.glUniform1f(loc, time.time()%100000)
        else:
            glUseProgram(0)

    def init_texture(self):
        self.texi = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, self.texi)   # 2d texture (x and y size)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
        glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, self.width, self.height, 0)

    def reshape(self, width, height):
        self.width = width
        self.height = height
        glViewport(0, 0, width, height)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        #if width > height:
        #    w = float(width) / float(height)
        #    glFrustum(-w, w, -1.0, 1.0, 5.0, 60.0)
        #
        #else:
        h = float(height) / float(width)
        glFrustum(-1.0, 1.0, -h, h, 5.0, 60.0)
        self.ratio = h
        glMatrixMode(GL_MODELVIEW)
        if self.shader_program is not None:
            loc = oglsh.glGetUniformLocation(self.shader_program, "screensize")
            oglsh.glUniform2f(loc, float(self.width), float(self.height))

    def process_data(self, data):
        sz = len(data)/2
        if sz == 0:
            return
        self.data = data
        #print(data)

        if self.simple_beat_detection or self.beat_detection:
            fft1 = np.fft.fft(self.data[:sz])[:256]
            fft2 = np.fft.fft(self.data[sz:])[:256]
            real1 = np.real(fft1*np.conj(fft1))
            real2 = np.real(fft2*np.conj(fft2))


            if self.simple_beat_detection:
                self.simple_beat_detect(real1, real2)
            else:
                self.beat_detect(real1, real2)

    def simple_beat_detect(self, real1, real2):
        def normalize(array):
            return array/np.nanmax(np.fabs(array))
        tp = np.array([
            np.sum(real1[15:]),
            np.sum(real1[4:15]),
            np.sum(real1[:4])])
        s1 = (np.sum(tp), tp[2], tp[1], tp[0])

        tp = np.array([
            np.sum(real2[15:]),
            np.sum(real2[4:15]),
            np.sum(real2[:4])])
        s2 = (np.sum(tp), tp[2], tp[1], tp[0])
        #store averages
        self.simple_stored[0] = np.mean([s1,s2], 0)

        var = 100*np.var(normalize(self.simple_stored[:50,0]))
        # 12 - 2,5, 2 -> 1,5
        c = 1.3+var/10.0
        mean = np.mean(self.simple_stored[:50,0])

        if c*mean < self.simple_stored[0][0]:
            self.simple_beat_detected = True

        self.simple_stored = np.roll(self.simple_stored, 1, 0)

    def beat_detect(self, real1, real2):
        prev = 0
        C = 4.0
        avg = np.mean(self.stored_fftl) +np.mean(real1)+np.mean(real2)
        if avg == 0:
            return
        for i in xrange(self.beats_to_detect):
            a = self.beat_limits[i]
            #count for each subband the energy
            self.stored_fftl[0][i] = np.sum(real1[prev:a])
            self.stored_fftr[0][i] = np.sum(real2[prev:a])
            prev = a

            avgl = np.mean(self.stored_fftl[:50,i])
            avgr = np.mean(self.stored_fftr[:50,i])
            #if i == 0 and avgl > 0.0:
            #    print(self.stored_fftl[0][i]/avgl, self.stored_fftr[0][i]/avgr)
            if avgl*C < self.stored_fftl[0][i]:
                self.beat_detected[i] |= 1
            if avgr*C < self.stored_fftr[0][i]:
                self.beat_detected[i] |= 2
            #if i == 0 and self.beat_detected[i] > 0:
            #    print('beat')

        self.stored_fftl = np.roll(self.stored_fftl, 1, 0)
        self.stored_fftr = np.roll(self.stored_fftr, 1, 0)

    def pcm_show(self, width, height):
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        sz = len(self.data)/2
        if sz == 0:
            return

        glLoadIdentity()
        glTranslate(0, 0, -10)
        glRotate(self.rotx, 1, 0, 0)
        glRotate(self.roty, 0, 1, 0)
        glRotate(self.rotz, 0, 0, 1)

        ttransz = self.ttransz
        if self.simple_beat_detected:
            self.ttransz = ttransz+0.2
            rand = np.random.rand(4) - 0.5
            ttransz = max(min(ttransz+rand[3]/5.0, 0.3), -0.3)
            self.trotz = max(min(self.trotz+rand[0], 1.5), -1.5)
            self.trotx = max(min(self.trotx+rand[1], 5), -5)
            self.troty = max(min(self.troty+rand[2], 5), -5)

        #create arrays for
        #normalize the data
        tmax = np.abs(np.max(self.data))*3
        if tmax == 0:
            tmax = 1.0
        data = self.data/tmax
        #data = self.data
        xs = np.linspace(-2., 2., sz)
        varr = np.column_stack( (xs, data[:sz]+0.5, np.zeros(sz), np.ones(sz)))
        varr2 = np.column_stack( (xs, data[sz:]-0.5, np.zeros(sz) , np.ones(sz)))

        energy_t = np.mean(self.simple_stored[:,0])
        energy_rate = self.simple_stored[1:2,1].mean()/max((energy_t, 0.1))
        glLineWidth(min(energy_rate*2, 5)+1.0)
        a = self.simple_stored[1]

        color = np.array([
                a[3],
                a[2],
                a[1], a[0]*0.8])
        glColor4fv(color/a[0])
        #glColor4fv([1.0, 1.0, 1.0, 0.8])

        glDisable(GL_TEXTURE_2D)
        glVertexPointerf(varr)
        glEnableClientState(GL_VERTEX_ARRAY)
        glDrawArrays(GL_LINE_STRIP, 0, len(varr))
        glDisableClientState(GL_VERTEX_ARRAY)

        glVertexPointerf(varr2)
        glEnableClientState(GL_VERTEX_ARRAY)
        glDrawArrays(GL_LINE_STRIP, 0, len(varr2))
        glDisableClientState(GL_VERTEX_ARRAY)
        
        glEnable(GL_TEXTURE_2D)
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
        glBindTexture(GL_TEXTURE_2D, self.texi)
        glEnable(GL_BLEND)
        glBlendFunc(GL_ONE, GL_ONE)

        glLoadIdentity()
        glColor4f(0.0, 0.0, 0.0, 0.96)
        glTranslatef(self.ttransx, self.ttransy, -10.0+self.ttransz)
        glRotate(self.trotx, 1, 0, 0)
        glRotate(self.troty, 0, 1, 0)
        glRotate(self.trotz, 0, 0, 1)
        glScalef(1.0, self.ratio, 1.0)
        glCallList(TEX_QUADS)

        #save the image
        glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, self.width, self.height, 0)
        glDisable(GL_TEXTURE_2D)
        
        self.rotz += 0.1
        #self.trotz = 0.5
        #self.troty = 20
        #self.trotx = 20
        #self.ttransx = 0.02
        self.ttransz = ttransz
        self.simple_beat_detected = False

    def update_beat_shader(self):
        loc = oglsh.glGetUniformLocation(self.shader_program, "currentTime")
        oglsh.glUniform1f(loc, time.time()%100000)
        
        loc = oglsh.glGetUniformLocation(self.shader_program, "beat_size_vec")
        
        self.historyarr = np.roll(self.historyarr, 1, 0)
        self.historyarr[0] = self.beat_detected
        #self.historyarr[0] = np.ones(28)
        #reset beats
        self.beat_detected = np.zeros(self.beats_to_detect, 'b')
        
        weights = np.linspace(1.0, 0.0, num=32)
        beat_size_vec = np.sum(self.historyarr.transpose()*weights, axis=1)/16.0
        oglsh.glUniform1fv(loc, 28, beat_size_vec)
        #sys.exit(1)


    def beat_show(self, width, height):
        glLoadIdentity()
        glClear(GL_COLOR_BUFFER_BIT)
        sz = len(self.data)/2
        if sz == 0:
            return
        avg = np.mean(self.stored_fftl)
        if avg == 0:
            return
        if self.shader_program is None:
            return
        self.update_beat_shader()

        glDisable(GL_LIGHTING)
        glDisable(GL_TEXTURE_2D)
        glTranslatef(0, 0, -10.0)
        glScalef(1.0, self.ratio, 1.0)
        glCallList(TEX_QUADS)

        """
        a = 0.2
        b = 0.7/(6*180)
        epsilon = 0.001
        ttransz = 1
        
        #generate the textures
        glDisable(GL_TEXTURE_2D)
        glEnable(GL_BLEND)
        glBlendFunc(GL_ONE, GL_ONE)
        glDisable(GL_DEPTH_TEST)
        glTranslatef(0, 0, -10)
        for i in xrange(self.beats_to_detect):
            glClear(GL_COLOR_BUFFER_BIT)
            if self.beat_detected[i] > 0:
                glColor3fv(self.palette[i])
                glPointSize((self.beats_to_detect-i)/2.0+10)
                glCallList(POINTS)
            glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
            glBlendFunc(GL_ONE, GL_ONE)
            #draw the already ready textures
            glEnable(GL_TEXTURE_2D)
            glBindTexture(GL_TEXTURE_2D, self.texis[i])
            glColor4f(0.0, 0.0, 0.0, 0.9)
            glPushMatrix()
            glTranslatef(0.0, 0.0, ttransz)
            glScalef(1.0, self.ratio, 1.0)
            glCallList(TEX_QUADS)
            glPopMatrix()
            
            h = 2*self.ratio
            
            #shadow edges
            glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
            glBindTexture(GL_TEXTURE_2D, self.shadow_tex)
            glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
            glPushMatrix()
            glScalef(1.0, self.ratio, 1.0)
            glCallList(TEX_QUADS)
            glPopMatrix()
            
            glBindTexture(GL_TEXTURE_2D, self.texis[i])
            
            #copy the result to our texture
            glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, self.width, self.height, 0)
            glDisable(GL_TEXTURE_2D)
        #textures ready
        #let's draw them
        glColor4f(0.0, 0.0, 0.0, 1.0)
        glClear(GL_COLOR_BUFFER_BIT)
        glEnable(GL_TEXTURE_2D)

        h = 2*self.ratio
        glBlendFunc(GL_ONE, GL_ONE)
        for i in xrange(0,self.beats_to_detect):
            glLoadIdentity()
            glRotatef(self.rotz, 0, 0, 1)
            glTranslatef(0, 0, -10.0+i*epsilon)
            angle = i*6.0/self.beats_to_detect*180
            glRotatef(angle, 0, 0, 1)
            glTranslatef(2*angle*b+a, 0, 0)
            glRotatef(-angle, 0, 0, 1)

            glBindTexture(GL_TEXTURE_2D, self.texis[i])
            glScalef(1.0, self.ratio, 1.0)
            glCallList(TEX_QUADS)
        
         
        self.rotz += -0.5
        """


    #display = beat_show
    display = pcm_show


